package com.carey.blog.action.util;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.jsoup.Jsoup;

import com.carey.blog.framework.latke.Keys;
import com.carey.blog.framework.latke.action.util.Paginator;
import com.carey.blog.framework.latke.model.Pagination;
import com.carey.blog.framework.latke.model.User;
import com.carey.blog.framework.latke.repository.FilterOperator;
import com.carey.blog.framework.latke.repository.Query;
import com.carey.blog.framework.latke.repository.RepositoryException;
import com.carey.blog.framework.latke.repository.SortDirection;
import com.carey.blog.framework.latke.util.CollectionUtils;
import com.carey.blog.framework.latke.util.Locales;
import com.carey.blog.listener.CareyBlogServletListener;
import com.carey.blog.model.ArchiveDate;
import com.carey.blog.model.Article;
import com.carey.blog.model.Comment;
import com.carey.blog.model.Common;
import com.carey.blog.model.Link;
import com.carey.blog.model.Preference;
import com.carey.blog.model.Skin;
import com.carey.blog.model.Statistic;
import com.carey.blog.repository.ArchiveDateRepository;
import com.carey.blog.repository.ArticleRepository;
import com.carey.blog.repository.CommentRepository;
import com.carey.blog.repository.LinkRepository;
import com.carey.blog.repository.PageRepository;
import com.carey.blog.repository.StatisticRepository;
import com.carey.blog.repository.TagRepository;
import com.carey.blog.repository.UserRepository;
import com.carey.blog.repository.impl.ArchiveDateGAERepository;
import com.carey.blog.repository.impl.ArticleGAERepository;
import com.carey.blog.repository.impl.CommentGAERepository;
import com.carey.blog.repository.impl.LinkGAERepository;
import com.carey.blog.repository.impl.PageGAERepository;
import com.carey.blog.repository.impl.StatisticGAERepository;
import com.carey.blog.repository.impl.TagGAERepository;
import com.carey.blog.repository.impl.UserGAERepository;
//import com.carey.blog.util.ArchiveDates;
import com.carey.blog.util.Articles;
import com.carey.blog.util.Tags;

/**
 * Filler utilities.
 * 
 */
public final class Filler {

	/**
	 * Logger.
	 */
	private static final Logger LOGGER = Logger.getLogger(Filler.class
			.getName());
	/**
	 * Article repository.
	 */
	private ArticleRepository articleRepository = ArticleGAERepository
			.getInstance();
	/**
	 * Comment repository.
	 */
	private CommentRepository commentRepository = CommentGAERepository
			.getInstance();
	/**
	 * Archive date repository.
	 */
	private ArchiveDateRepository archiveDateRepository = ArchiveDateGAERepository
			.getInstance();
	/**
	 * Tag repository.
	 */
	private TagRepository tagRepository = TagGAERepository.getInstance();
	/**
	 * Article utilities.
	 */
	private Articles articleUtils = Articles.getInstance();
	/**
	 * Tag utilities.
	 */
	private Tags tagUtils = Tags.getInstance();
	/**
	 * Link repository.
	 */
	private LinkRepository linkRepository = LinkGAERepository.getInstance();
	/**
	 * Page repository.
	 */
	private PageRepository pageRepository = PageGAERepository.getInstance();
	/**
	 * Archive date utilities.
	 */
	//private ArchiveDates archiveDateUtils = ArchiveDates.getInstance();
	/**
	 * Statistic repository.
	 */
	private StatisticRepository statisticRepository = StatisticGAERepository
			.getInstance();
	/**
	 * User repository.
	 */
	private UserRepository userRepository = UserGAERepository.getInstance();
	/**
	 * {@code true} for published.
	 */
	private static final boolean PUBLISHED = true;

	/**
	 * Fills articles in index.ftl.
	 * 
	 * @param dataModel
	 *            data model
	 * @param currentPageNum
	 *            current page number
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillIndexArticles(final Map<String, Object> dataModel,
			final int currentPageNum, final JSONObject preference)
			throws Exception {
		final int pageSize = preference
				.getInt(Preference.ARTICLE_LIST_DISPLAY_COUNT);
		final int windowSize = preference
				.getInt(Preference.ARTICLE_LIST_PAGINATION_WINDOW_SIZE);

		final Query query = new Query()
				.setCurrentPageNum(currentPageNum)
				.setPageSize(pageSize)
				.addFilter(Article.ARTICLE_IS_PUBLISHED, FilterOperator.EQUAL,
						PUBLISHED);

		if (preference.getBoolean(Preference.ENABLE_ARTICLE_UPDATE_HINT)) {
			query.addSort(Article.ARTICLE_UPDATE_DATE, SortDirection.DESCENDING);
		} else {
			query.addSort(Article.ARTICLE_CREATE_DATE, SortDirection.DESCENDING);
		}

		query.addSort(Article.ARTICLE_PUT_TOP, SortDirection.DESCENDING);

		final JSONObject result = articleRepository.get(query);
		final int pageCount = result.getJSONObject(Pagination.PAGINATION)
				.getInt(Pagination.PAGINATION_PAGE_COUNT);

		final List<Integer> pageNums = Paginator.paginate(currentPageNum,
				pageSize, pageCount, windowSize);
		if (0 != pageNums.size()) {
			dataModel
					.put(Pagination.PAGINATION_FIRST_PAGE_NUM, pageNums.get(0));
			dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM,
					pageNums.get(pageNums.size() - 1));
		}
		dataModel.put(Pagination.PAGINATION_PAGE_COUNT, pageCount);
		dataModel.put(Pagination.PAGINATION_PAGE_NUMS, pageNums);

		final List<JSONObject> articles = com.carey.blog.framework.latke.util.CollectionUtils
				.jsonArrayToList(result.getJSONArray(Keys.RESULTS));
		putArticleExProperties(articles, preference);

		dataModel.put(Article.ARTICLES, articles);
	}

	/**
	 * Fills links.
	 * 
	 * @param dataModel
	 *            data model
	 * @throws JSONException
	 *             json exception
	 * @throws RepositoryException
	 *             repository exception
	 */
	private void fillLinks(final Map<String, Object> dataModel)
			throws JSONException, RepositoryException {
		final Map<String, SortDirection> sorts = new HashMap<String, SortDirection>();
		sorts.put(Link.LINK_ORDER, SortDirection.ASCENDING);
		final Query query = new Query().addSort(Link.LINK_ORDER,
				SortDirection.ASCENDING);
		final JSONObject linkResult = linkRepository.get(query);
		final List<JSONObject> links = com.carey.blog.framework.latke.util.CollectionUtils
				.jsonArrayToList(linkResult.getJSONArray(Keys.RESULTS));

		dataModel.put(Link.LINKS, links);
	}

	/**
	 * Fills most used tags.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillMostUsedTags(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling most used tags....");
		final int mostUsedTagDisplayCnt = preference
				.getInt(Preference.MOST_USED_TAG_DISPLAY_CNT);

		final List<JSONObject> tags = tagRepository
				.getMostUsedTags(mostUsedTagDisplayCnt);
		tagUtils.removeForUnpublishedArticles(tags);

		dataModel.put(Common.MOST_USED_TAGS, tags);
	}

	/**
	 * Fills archive dates.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillArchiveDates(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling archive dates....");
		final List<JSONObject> archiveDates = archiveDateRepository
				.getArchiveDates();

		final String localeString = preference
				.getString(Preference.LOCALE_STRING);
		final String language = Locales.getLanguage(localeString);

		for (final JSONObject archiveDate : archiveDates) {
			final Date date = (Date) archiveDate.get(ArchiveDate.ARCHIVE_DATE);
			final String dateString = ArchiveDate.DATE_FORMAT.format(date);
			final String[] dateStrings = dateString.split("/");
			final String year = dateStrings[0];
			final String month = dateStrings[1];
			archiveDate.put(ArchiveDate.ARCHIVE_DATE_YEAR, year);

			if ("en".equals(language)) {
				archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH,
						CareyBlogServletListener.EN_MONTHS.get(month));
			} else {
				archiveDate.put(ArchiveDate.ARCHIVE_DATE_MONTH, month);
			}
		}

		dataModel.put(ArchiveDate.ARCHIVE_DATES, archiveDates);
	}

	/**
	 * Fills most view count articles.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillMostViewCountArticles(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling the most view count articles....");
		final int mostCommentArticleDisplayCnt = preference
				.getInt(Preference.MOST_VIEW_ARTICLE_DISPLAY_CNT);
		final List<JSONObject> mostViewCountArticles = articleRepository
				.getMostViewCountArticles(mostCommentArticleDisplayCnt);

		dataModel.put(Common.MOST_VIEW_COUNT_ARTICLES, mostViewCountArticles);
	}

	/**
	 * Fills most comments articles.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillMostCommentArticles(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling most comment articles....");
		final int mostCommentArticleDisplayCnt = preference
				.getInt(Preference.MOST_COMMENT_ARTICLE_DISPLAY_CNT);
		final List<JSONObject> mostCommentArticles = articleRepository
				.getMostCommentArticles(mostCommentArticleDisplayCnt);

		dataModel.put(Common.MOST_COMMENT_ARTICLES, mostCommentArticles);
	}

	/**
	 * Fills post articles recently.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillRecentArticles(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		final int recentArticleDisplayCnt = preference
				.getInt(Preference.RECENT_ARTICLE_DISPLAY_CNT);

		final List<JSONObject> recentArticles = articleRepository
				.getRecentArticles(recentArticleDisplayCnt);

		dataModel.put(Common.RECENT_ARTICLES, recentArticles);
	}

	/**
	 * Fills post comments recently.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillRecentComments(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling recent comments....");
		final int recentCommentDisplayCnt = preference
				.getInt(Preference.RECENT_COMMENT_DISPLAY_CNT);

		final List<JSONObject> recentComments = commentRepository
				.getRecentComments(recentCommentDisplayCnt);

		// Erase email for security reason
		for (final JSONObject comment : recentComments) {
			final String content = comment.getString(Comment.COMMENT_CONTENT)
					.replaceAll(CareyBlogServletListener.ENTER_ESC, "&nbsp;");
			comment.put(Comment.COMMENT_CONTENT, Jsoup.parse(content).text());
			comment.remove(Comment.COMMENT_EMAIL);
		}

		dataModel.put(Common.RECENT_COMMENTS, recentComments);
	}

	/**
	 * Fills <article-footer.ftl>.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillBlogFooter(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.finer("Filling footer....");
		final String blogTitle = preference.getString(Preference.BLOG_TITLE);
		dataModel.put(Preference.BLOG_TITLE, blogTitle);
		final String blogHost = preference.getString(Preference.BLOG_HOST);
		dataModel.put(Preference.BLOG_HOST, blogHost);

		dataModel.put(Common.VERSION, CareyBlogServletListener.VERSION);
		dataModel.put(Common.YEAR,
				String.valueOf(Calendar.getInstance().get(Calendar.YEAR)));
	}

	/**
	 * Fills <article-header.ftl>.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillBlogHeader(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.fine("Filling header....");
		dataModel.put(Preference.LOCALE_STRING,
				preference.getString(Preference.LOCALE_STRING));
		dataModel.put(Preference.BLOG_TITLE,
				preference.getString(Preference.BLOG_TITLE));
		dataModel.put(Preference.BLOG_SUBTITLE,
				preference.getString(Preference.BLOG_SUBTITLE));
		dataModel.put(Preference.HTML_HEAD,
				preference.getString(Preference.HTML_HEAD));
		dataModel.put(Preference.META_KEYWORDS,
				preference.getString(Preference.META_KEYWORDS));
		dataModel.put(Preference.META_DESCRIPTION,
				preference.getString(Preference.META_DESCRIPTION));

		final JSONObject result = userRepository.get(new Query());
		final JSONArray users = result.getJSONArray(Keys.RESULTS);
		final List<JSONObject> userList = CollectionUtils
				.jsonArrayToList(users);
		dataModel.put(User.USERS, userList);
		for (final JSONObject user : userList) {
			user.remove(User.USER_EMAIL);
		}

		final String skinDirName = preference.getString(Skin.SKIN_DIR_NAME);
		dataModel.put(Skin.SKIN_DIR_NAME, skinDirName);

		fillPageNavigations(dataModel);
		fillStatistic(dataModel);
	}

	/**
	 * Fills <article-side.ftl>.
	 * 
	 * @param dataModel
	 *            data model
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillSide(final Map<String, Object> dataModel,
			final JSONObject preference) throws Exception {
		LOGGER.fine("Filling side....");
		fillLinks(dataModel);
		// fillRecentArticles(dataModel, preference);
		fillRecentComments(dataModel, preference);
		fillMostUsedTags(dataModel, preference);
		fillMostCommentArticles(dataModel, preference);
		fillMostViewCountArticles(dataModel, preference);
		fillArchiveDates(dataModel, preference);

		final String noticeBoard = preference
				.getString(Preference.NOTICE_BOARD);
		dataModel.put(Preference.NOTICE_BOARD, noticeBoard);
	}

	/**
	 * Fills articles in <index.ftl> for skin {@literal valentine}. The left part
	 * contains administrator's articles, the right part contains another user's
	 * articles.
	 * 
	 * @param dataModel
	 *            data model
	 * @param leftCurrentPageNum
	 *            left part page number
	 * @param rightCurrentPageNum
	 *            right part current page number
	 * @param preference
	 *            the specified preference
	 * @throws Exception
	 *             exception
	 */
	public void fillIndexArticlesForValentine(
			final Map<String, Object> dataModel, final int leftCurrentPageNum,
			final int rightCurrentPageNum, final JSONObject preference)
			throws Exception {
		LOGGER.finer("Filling article list for skin valentine....");
		final int pageSize = preference
				.getInt(Preference.ARTICLE_LIST_DISPLAY_COUNT);
		final int windowSize = preference
				.getInt(Preference.ARTICLE_LIST_PAGINATION_WINDOW_SIZE);

		final JSONObject result = userRepository.get(new Query());
		final JSONArray users = result.getJSONArray(Keys.RESULTS);

		final String adminEmail = users.getJSONObject(0).getString(
				User.USER_EMAIL);
		final List<JSONObject> articlesL = fillPart(preference,
				leftCurrentPageNum, pageSize, windowSize, dataModel,
				Common.LEFT_PART_NAME, adminEmail);

		List<JSONObject> articlesR = new ArrayList<JSONObject>();
		if (1 < users.length()) {
			final String anotherUserEmail = users.getJSONObject(1).getString(
					User.USER_EMAIL);
			articlesR = fillPart(preference, rightCurrentPageNum, pageSize,
					windowSize, dataModel, Common.RIGHT_PART_NAME,
					anotherUserEmail);
		}
		dataModel.put(Article.ARTICLES + Common.RIGHT_PART_NAME, articlesR);

		final List<JSONObject> articles = new ArrayList<JSONObject>();

		articles.addAll(articlesL);
		articles.addAll(articlesR);
		putArticleExProperties(articles, preference);

		dataModel.put(Article.ARTICLES, articles);
	}

	/**
	 * Fills page navigations.
	 * 
	 * @param dataModel
	 *            data model
	 * @throws Exception
	 *             exception
	 */
	private void fillPageNavigations(final Map<String, Object> dataModel)
			throws Exception {
		LOGGER.finer("Filling page navigations....");
		final List<JSONObject> pages = pageRepository.getPages();

		dataModel.put(Common.PAGE_NAVIGATIONS, pages);
	}

	/**
	 * Fills statistic.
	 * 
	 * @param dataModel
	 *            data model
	 * @throws Exception
	 *             exception
	 */
	private void fillStatistic(final Map<String, Object> dataModel)
			throws Exception {
		LOGGER.finer("Filling statistic....");
		final JSONObject statistic = statisticRepository
				.get(Statistic.STATISTIC);

		statistic.remove(Statistic.STATISTIC_BLOG_ARTICLE_COUNT);
		statistic.remove(Statistic.STATISTIC_BLOG_COMMENT_COUNT);

		dataModel.put(Statistic.STATISTIC, statistic);
	}

	/**
	 * Fills left(admin) or right part for skin {@literal valentine} with the
	 * specified parameters.
	 * 
	 * @param preference
	 *            the specified preference
	 * @param currentPageNum
	 *            the specified current page number
	 * @param pageSize
	 *            the specified page size
	 * @param windowSize
	 *            the specified window size
	 * @param dataModel
	 *            the specified data model
	 * @param partName
	 *            the specified part name
	 * @param authorEmail
	 *            the specified authror email
	 * @return the filled articles
	 * @throws JSONException
	 *             json exception
	 * @throws RepositoryException
	 *             repository exception
	 */
	private List<JSONObject> fillPart(final JSONObject preference,
			final int currentPageNum, final int pageSize, final int windowSize,
			final Map<String, Object> dataModel, final String partName,
			final String authorEmail) throws JSONException, RepositoryException {
		LOGGER.log(Level.FINEST, "Filling part[name={0}, authorEmail={1}]",
				new String[] { partName, authorEmail });

		final Query query = new Query().addFilter(Article.ARTICLE_IS_PUBLISHED,
				FilterOperator.EQUAL, PUBLISHED)
				.addFilter(Article.ARTICLE_AUTHOR_EMAIL, FilterOperator.EQUAL,
						authorEmail);

		if (preference.getBoolean(Preference.ENABLE_ARTICLE_UPDATE_HINT)) {
			query.addSort(Article.ARTICLE_UPDATE_DATE, SortDirection.DESCENDING);
		} else {
			query.addSort(Article.ARTICLE_CREATE_DATE, SortDirection.DESCENDING);
		}

		query.addSort(Article.ARTICLE_PUT_TOP, SortDirection.DESCENDING);
		final JSONObject result = articleRepository.get(query);
		final int pageCount = result.getJSONObject(Pagination.PAGINATION)
				.getInt(Pagination.PAGINATION_PAGE_COUNT);
		final List<Integer> pageNums = Paginator.paginate(currentPageNum,
				pageSize, pageCount, windowSize);
		if (0 != pageNums.size()) {
			dataModel.put(Pagination.PAGINATION_FIRST_PAGE_NUM + partName,
					pageNums.get(0));
			dataModel.put(Pagination.PAGINATION_LAST_PAGE_NUM + partName,
					pageNums.get(pageNums.size() - 1));
		}
		dataModel.put(Pagination.PAGINATION_PAGE_COUNT + partName, pageCount);
		dataModel.put(Pagination.PAGINATION_PAGE_NUMS + partName, pageNums);
		final List<JSONObject> ret = com.carey.blog.framework.latke.util.CollectionUtils
				.jsonArrayToList(result.getJSONArray(Keys.RESULTS));
		for (final JSONObject article : ret) {
			if (preference.getBoolean(Preference.ENABLE_ARTICLE_UPDATE_HINT)) {
				article.put(Common.HAS_UPDATED,
						articleUtils.hasUpdated(article));
			} else {
				article.put(Common.HAS_UPDATED, false);
			}
		}

		dataModel.put(Article.ARTICLES + partName, ret);

		return ret;
	}

	/**
	 * Puts ext properties for the specified articles.
	 * 
	 * <p>
	 * Article ext properties:
	 * <ul>
	 * <li>{@value Common#HAS_UPDATED}: boolean</li>
	 * <li>{@value Common#AUTHOR_ID}: ""</li>
	 * <li>{@value Common#AUTHOR_NAME}: ""</li>
	 * </ul>
	 * </p>
	 * 
	 * @param articles
	 *            the specified articles
	 * @param preference
	 *            the specified preference
	 * @throws JSONException
	 *             json exception
	 */
	public void putArticleExProperties(final List<JSONObject> articles,
			final JSONObject preference) throws JSONException {
		for (final JSONObject article : articles) {
			if (preference.getBoolean(Preference.ENABLE_ARTICLE_UPDATE_HINT)) {
				article.put(Common.HAS_UPDATED,
						articleUtils.hasUpdated(article));
			} else {
				article.put(Common.HAS_UPDATED, false);
			}

			// Puts author name
			final JSONObject author = articleUtils.getAuthor(article);
			final String authorName = author.getString(User.USER_NAME);
			article.put(Common.AUTHOR_NAME, authorName);
			final String authorId = author.getString(Keys.OBJECT_ID);
			article.put(Common.AUTHOR_ID, authorId);
		}
	}

	/**
	 * Gets the {@link Filler} singleton.
	 * 
	 * @return the singleton
	 */
	public static Filler getInstance() {
		return SingletonHolder.SINGLETON;
	}

	/**
	 * Private default constructor.
	 */
	private Filler() {
	}

	/**
	 * Singleton holder.
	 * 
	 */
	private static final class SingletonHolder {

		/**
		 * Singleton.
		 */
		private static final Filler SINGLETON = new Filler();

		/**
		 * Private default constructor.
		 */
		private SingletonHolder() {
		}
	}
}
